
# Mandatory Files

$MandatoryFilesInBaseFolder = @(
    '\parameters.txt'
)

$MandatoryFilesInBaseProjectFolder = @(
    '\arm_template_parameters.json'
)

$MandatoryFilesInHotfixFolder = @()

# Add path to these folders here. The script will otherwise prompt for the paths.
# $BaseFolder = ''
# $ProjectFolder = ''
# $FixesFolder = ''


# Declare functions

<#
    Cleares the screen and writes a header with a message.
    $Message: Mandatory. The message that is displayed.
    $ForegroundColor: Optional. Overwrites the default foreground color.
#>
function Write-LSHeader
{
    Param([string]$Message, [string]$ForegroundColor)

    Clear-Host
    Write-Host '*******************************'
    if ($ForegroundColor) { Write-Host $Message -ForegroundColor $ForegroundColor } else { Write-Host $Message }
    Write-Host '*******************************'
}

<#
    Checks if path has the mandatory files as child items.
    $PathToConsider. Mandatory. Mandatory files are located with this path as a prefix.
    Returns an array of mandatory file paths that are missing.
#>
function Test-LSFolder
{
    Param ([string]$PathToConsider, [string]$ValidateAs)

    if ($ValidateAs -eq 'BaseFolder') {
        $MandatoryFiles = $MandatoryFilesInBaseFolder
    }
    elseif ($ValidateAs -eq 'BaseProjectFolder') {
        $MandatoryFiles = $MandatoryFilesInBaseProjectFolder
    }
    elseif ($ValidateAs -eq 'HotfixFolder') {
        $MandatoryFiles = $MandatoryFilesInHotfixFolder
    }

    $FailedPaths = @()
    foreach ($MFile in $MandatoryFiles) {
        $JoinedPath = Join-Path -Path $PathToConsider -ChildPath $MFile
        $IsValidMFile = Test-Path -Path $JoinedPath
        if (!$IsValidMFile) {
            $FailedPaths += $JoinedPath
        }
    }
    return $FailedPaths
}

<#
    Provides a valid path.
    $ValidateAs: Mandatory. ('BaseFolder' or 'BaseProjectFolder').
    $PathToConsider: Optional. Accepts this path if it meets criteria.
    $DisplayErrorMessage: Optional. ($true or $false). If true, then it shows the missing files to the user.
    $HeaderMessage: Mandatory. This is the message that is displayed in the header. 
#>
function Get-LSPath
{
    Param ([string]$ValidateAs, [string]$PathToConsider, [boolean]$DisplayErrorMessage, [string]$HeaderMessage)

    if (!$PathToConsider) {
        return Request-LSPath -ValidateAs $ValidateAs -HeaderMessage $HeaderMessage
    }

    # Test the path.
    $failedMandatoryPaths = Test-LSFolder -ValidateAS $ValidateAs -PathToConsider $PathToConsider

    if ($failedMandatoryPaths.Count -eq 0) {
        # The path meets criteria.
        if (($DisplayErrorMessage -eq $false) -and ($HeaderMessage -ne $null)) {
            Write-LSHeader -Message $HeaderMessage
        }
        return $PathToConsider;
    }
    else {
        # The path doesn't meet criteria.
        if ($DisplayErrorMessage) {
            # Displays what mandatory files that are missing and asks for a valid path.
            Write-LSHeader -Message $HeaderMessage
            Write-Host 'The path you suggested does not contain the necessary files:' -ForegroundColor 'RED'
            foreach ($MPath in $failedMandatoryPaths) {
                Write-Host $MPath
            }
            Write-Host 'Try Again.' -ForegroundColor "RED"
            return Request-LSPath -ValidateAs $ValidateAs -headerMessage $HeaderMessage -SkipHeader $true
        }
        else {
            # Asks for a valid path.
            return Request-LSPath -ValidateAS $ValidateAs -HeaderMessage $HeaderMessage
        }
    }
}

<#
    Prompts the user for a path.
    $ValidateAs: Mandatory. ('BaseFolder' or 'BaseProjectFolder').
    $HeaderMessage: Optional. If set, then the console is cleared and a header is displayed with this message.  
#>
function Request-LSPath
{
    Param ([string]$ValidateAs, [string]$HeaderMessage, [boolean]$SkipHeader = $false)

    if ($HeaderMessage -and !$SkipHeader)
    {
        Write-LSHeader -Message $HeaderMessage
    }

    $newPath = Read-Host -Prompt 'Please enter full path'
    return Get-LSPath -ValidateAs $ValidateAs -PathToConsider $newPath -DisplayErrorMessage $true -HeaderMessage $HeaderMessage
}

<#
    Gets a valid base folder path from user and adds it to the $InstallParameters input and returns it.
    $InstallParameters: Mandatory. Pipeline input. Hashtable with necessary parameters to install LS Insight successfully.
#>
function Get-LSBaseFolder {
    [CmdletBinding()]
    Param([Parameter(ValueFromPipeline)]$InstallParameters)

    # Ask user for base folder path.
    $answer = Get-LSPath -ValidateAs 'BaseFolder' -PathToConsider $BaseFolder -DisplayErrorMessage $false -HeaderMessage "Full path to the template folder is needed. This was the downloaded folder which included the deployment script, file named parameters.txt and a folder named 'arm_template'."

    # Set base folder parameter.
    return $InstallParameters += @{"Base folder" = $answer}
}

<#
    Gets a valid project folder path from user and adds it to the $InstallParameters input and returns it.
    $InstallParameters: Mandatory. Pipeline input. Hashtable with necessary parameters to install LS Insight successfully.
#>
function Get-LSProjectFolder {
    [CmdletBinding()]
    Param([Parameter(ValueFromPipeline)]$InstallParameters)

    # Ask user for project folder path.
    $answer = Get-LSPath -ValidateAs 'BaseProjectFolder' -PathToConsider $ProjectFolder -HeaderMessage 'Full path to the project folder is needed. This folder was created when the LS Insight (LS Central) was set up during the onboarding process. The folder is named like this "YYYYMMDD-MyProject"'

    return $InstallParameters += @{"Project folder" = $answer}
}

<#
    Asks user to confirm install parameters.
    $InstallParameters. Mandatory. Pipeline input. Hashtable with collected parameters.
#>
function Get-LSInstallParametersConfirmation
{
    [CmdletBinding()]
    Param([Parameter(ValueFromPipeline)]$InstallParameters)

    # Display install parameters to user.
    Write-Host "$($InstallParameters | Format-Table | Out-String)"

    # Ask for user to confirm parameters. (Accepts 'y' and 'yes' case insensitive).
    $answer = Read-Host -Prompt "Is this correct? (Write 'y' to confirm)"
    $answer = $answer.ToLower()

    # Set 'confirm' flag to true
    if (($answer -eq 'y') -or ($answer -eq 'yes')) {
        $InstallParameters += @{"confirmed" = $true}
    }

    return $InstallParameters
}

<#
    Returns prerequisites that are not met.
    (Easy to add more prerequisites if necessary)
#>
function Get-LSPrerequisitesNotMet
{
    $PrerequisitesNotMet = @()
    # $PrerequisitesNotMet += 'Take out the trash'
    # $PrerequisitesNotMet += 'Clean your room'
    # $PrerequisitesNotMet += 'Be nice'
    if (!(Get-Module -ListAvailable -Name Az.*)) {
        # Module Az doesn't exist
        $PrerequisitesNotMet += 'You are missing the Az module. Please install it before installing LS Insight'
    }
    return $PrerequisitesNotMet
}

<#
    Checks if all prerequisites are met. Exits script if not met.
#>
function Test-LSPrerequisites {
    # Check for prerequisites.
    $PreNotMet = Get-LSPrerequisitesNotMet
    if ($PreNotMet.count -ne 0)
    {
        # Prerequisites are not met.
        Write-LSHeader -Message 'These prerequisites are not met:' -ForegroundColor 'RED'
        foreach ($pre in $PreNotMet) {
            Write-Host "- $pre"
        }
        Write-Host
        exit
    }
    return
}

<#
    Changes TimeSpan duration and returns it as a readable string. E.g. 3 hours, 21 minutes and 8 seconds.
    $Duration: Mandatory
#>
function Get-LSHumanizeDuration {
    Param([TimeSpan]$Duration)

    if (!$Duration) {
        return
    }

    # Split the timespan into variables.
    $Hours = $Duration.Hours
    $Minutes = $Duration.Minutes
    $Seconds = $Duration.Seconds

    $ReturnString = ''

    # Generate string.
    if ($Hours -gt 0) {
        $ReturnString += "$Hours hours, "
    }
    if (($Minutes -gt 0) -or ($Hours -gt 0)) {
        $ReturnString += "$Minutes minutes and "
    }
    if (($Seconds -gt 0) -or ($Hours -gt 0) -or ($Minutes -gt 0)) {
        $ReturnString += "$Seconds seconds"
    }
    return $ReturnString
}

<#
    Lists down all the errors that occurred during installation. Creates error log file in project folder.
    $InstallParameters. Mandatory. Hashtable that stores the errors if there are any.
#>
function Publish-LSErrors {

    Param ([Parameter(ValueFromPipeline)]$InstallParameters)

    $AllErrors = @()
    if ($InstallParameters["HotfixInstallationErrors"].Count -gt 0) {
        Write-Host "Hotfix - Errors:"
        foreach ($err in $InstallParameters["HotfixInstallationErrors"]) {
            Write-Host "- $($err.MessageFromDev)"
        }
        $AllErrors += $InstallParameters["HotfixInstallationErrors"]
    }

    if ($InstallParameters["Project folder"] -and ($AllErrors.Count -gt 0)) {
        $ErrorFilePath = Join-Path -Path $InstallParameters["Project folder"] -ChildPath 'ErrorLog.txt'
        $AllErrors | ConvertTo-Json | Out-File -FilePath $ErrorFilePath
    }

    return $InstallParameters
}

<#
    Appends a list of the hotfixes, applied to the LS Insight deployment, to the Readme text file.
    $InstallParameters. Mandatory. Hashtable that stores HotfixesInfo and ProjectFolder.
#>
function Publish-LSUpdateLog {
    Param ([Parameter(ValueFromPipeline)]$InstallParameters)

    # Append update log to the Readme.txt if both the project folder and 
    if ($InstallParameters["Project folder"] -and ($InstallParameters["HotfixesInfo"].Count -gt 0)) {

        # Get a pretty timestamp.
        $Date = Get-Date -Format g

        # Get a path to the Readme.txt from the initial LS Insight deployment.
        $ReadMePath = Join-Path -Path $InstallParameters["Project folder"] -ChildPath 'Readme.txt'

        # Insert update log into Readme.txt.
        Add-Content $ReadMePath -Value "" # Newline.
        Add-Content $ReadMePath -Value "" # Newline.
        Add-Content $ReadMePath -Value "*** Update log - $Date ***"
        $InstallParameters["HotfixesInfo"] | ForEach-Object { Add-Content $ReadMePath -Value "$($_.Status) - $($_.Name)" } 
    }

    return $InstallParameters
}

<#
    Creates a farewell message for the user and lists down basic installation statistics and errors if any.
    $InstallParameters: Optional. Pipeline input. Hashtable returned from Invoke-LSInsightInstall.
#>
function Write-LSFooter {
    Param ([Parameter(ValueFromPipeline)]$InstallParameters)

    $HotfixResults = $InstallParameters["Hotfix Results"]

    # Write header.
    Write-LSHeader -Message "Setup Finished....and may I say - YOU LOOK AWESOME TODAY!"

    # Write install statistics.
    if ($HotfixResults) {
        $ErrorCount = $InstallParameters["HotfixInstallationErrors"].Count
        Write-Host "LS Insight (Hotfixes). Duration: $(Get-LSHumanizeDuration $HotfixResults.duration). With $ErrorCount errors."
    }

    # List down all the errors.
    $InstallParameters | Publish-LSErrors | Publish-LSUpdateLog | Out-Null

    return $InstallParameters
}

function Get-LSHotfixFolder {
    param (
        [Parameter(ValueFromPipeline)]
        $InstallParameters
    )
    
    # Ask user for project folder path.
    $answer = Get-LSPath -ValidateAs 'HotfixFolder' -PathToConsider $FixesFolder -HeaderMessage 'Full path to the hotfix folder is needed. This folder usually is in the same folder as this script.'

    return $InstallParameters += @{"HotfixFolderPath" = $answer}
}

function Write-LSHotfixInstallStatus {
    param (
        [Parameter(ValueFromPipeline)]
        $InstallParameters
    )

    $Hotfixes = $InstallParameters["HotfixesInfo"]

    $TotalHotfixes = $Hotfixes.Count
    $AppliedHotfixes = @()
    $AppliedHotfixes += ($Hotfixes | Where-Object { ($_.Status -eq "Failed") -or ($_.Status -eq "Finished") })
    Write-LSHeader -Message "Updating LS Insight ($($AppliedHotfixes.Count)/$TotalHotfixes)"

    for ($i = 0; $i -lt $Hotfixes.Count; $i++) {
        if ($Hotfixes[$i]["Status"] -eq "Not Started") {
            Write-Host $Hotfixes[$i].Name
        }
        elseif ($Hotfixes[$i]["Status"] -eq "In Progress") {
            Write-Host $Hotfixes[$i].Name -ForegroundColor Yellow
        }
        elseif ($Hotfixes[$i]["Status"] -eq "Failed") {
            Write-Host $Hotfixes[$i].Name -ForegroundColor Red
        }
        elseif ($Hotfixes[$i]["Status"] -eq "Finished") {
            Write-Host $Hotfixes[$i].Name -ForegroundColor Green
        }
    }
    
}

function Invoke-LSInsightHotFixes {
    param (
        [Parameter(ValueFromPipeline)]
        $InstallParameters
    )

    $InstallParameters += @{
        "HotfixInstallationErrors" = @()
    }

    # Get all hotfixes.
    $Hotfixes = Get-ChildItem -Path $InstallParameters["HotfixFolderPath"] | Where-Object { $_.Extension -eq '.json' } | Sort-Object -Property Name
    $HotfixesInfo = @()

    # Create basic info hashtable for each hotfix and store them all in an array.
    foreach ($Hotfix in $Hotfixes) {
        $Progress = @{}
        $Progress += @{
            "Name" = $Hotfix.basename
            "Status" = "Not Started"
            "Hotfix" = $Hotfix
        }
        $HotfixesInfo += $Progress
    }

    # Store basic info about each hotfix in $InstallParameters
    $InstallParameters += @{ "HotfixesInfo" = $HotfixesInfo }

    # Get parameter file.
    $ParameterPath = Join-Path -Path $InstallParameters["Base folder"] -ChildPath "parameters.txt"
    $Parameters = Get-Content $ParameterPath -Encoding UTF8| Out-String | ConvertFrom-StringData

    # Get values from parameter file.
    $SubscriptionID = $Parameters['$subscriptionId']
    $ResourceGroupName = $Parameters['$ResourceGroupName']
    
    # Get ProjectTemplateParameterFile.
    $ProjectTemplateParameterFile = Join-Path -Path $InstallParameters["Project folder"] -ChildPath 'arm_template_parameters.json'

    Write-LSHeader -Message "Please log in to your Azure account. (A login window should open up in the background)."

    # Connect to Azure.
    Connect-AzAccount -ErrorAction SilentlyContinue -ErrorVariable ConnectError | Out-Null
    if ($ConnectError) {
        $InstallParameters["BaseInstallationErrors"] += @{
            "MessageFromDev" = "Error occurred while logging into Azure account."
            "ErrorMessage" = $ConnectError.Exception[0].Message
            "Error" = $ConnectError
        }
    }

    # Select subscription.
    Select-AzSubscription -SubscriptionID $SubscriptionID -ErrorAction SilentlyContinue -ErrorVariable SelectSubscriptionError | Out-Null
    if ($SelectSubscriptionError) {
        $InstallParameters["BaseInstallationErrors"] += @{
            "MessageFromDev" = "Error occurred while selecting subscription."
            "ErrorMessage" = $SelectSubscriptionError.Exception[0].Message
            "Error" = $SelectSubscriptionError
        }
    }

    # Start the clock. Used to measure duration of applying all the hotfixes.
    $StartTime = Get-Date

    # Begin applying each hotfix, one at a time.
    for ($i = 0; $i -lt $InstallParameters["HotfixesInfo"].Count; $i++) {

        # Allow the progress visual to display every step.
        $InstallParameters | Write-LSHotfixInstallStatus | Out-Null
        Start-Sleep -s 2

        # Mark the current hotfix as 'In Progress' and display visual representation of the update progress.
        $InstallParameters["HotfixesInfo"][$i]["Status"] = "In Progress"
        $InstallParameters | Write-LSHotfixInstallStatus | Out-Null
        
        # Easier to read.
        $ThisHotfix = $InstallParameters["HotfixesInfo"][$i]
        
        # Get ProjectTemplateFile.
        $HotfixTemplateFile = $ThisHotfix['Hotfix'].FullName
        
        # Apply hotfix.
        New-AzResourceGroupDeployment `
        -ResourceGroupName $ResourceGroupName `
        -TemplateFile $HotfixTemplateFile `
        -TemplateParameterFile $ProjectTemplateParameterFile `
        -Force -ErrorAction SilentlyContinue -ErrorVariable NewResourceGroupDeploymentError | Out-Null
        
        if ($NewResourceGroupDeploymentError) {
            # If previous deployment failed, we will try again.
            
            # Apply hotfix again.
            New-AzResourceGroupDeployment `
            -ResourceGroupName $ResourceGroupName `
            -TemplateFile $HotfixTemplateFile `
            -TemplateParameterFile $ProjectTemplateParameterFile `
            -Force -ErrorAction SilentlyContinue -ErrorVariable NewResourceGroupDeploymentError2 | Out-Null
            
            if ($NewResourceGroupDeploymentError2) {
                # Store error if both deployments failed.
                $InstallParameters["HotfixInstallationErrors"] += @{
                    "MessageFromDev" = $ThisHotfix.Name
                    "ErrorMessage" = $NewResourceGroupDeploymentError2.Exception[0].Message
                    "Error" = $NewResourceGroupDeploymentError2
                }
                
                # Update status for current hotfix.
                $InstallParameters["HotfixesInfo"][$i]["Status"] = "Failed"
            }
        }
        
        # Mark current hotfix as finished.
        if ($InstallParameters["HotfixesInfo"][$i]["Status"] -ne 'Failed') {
            $InstallParameters["HotfixesInfo"][$i]["Status"] = "Finished"
        }

        #  Update visual representation of the update progress.
        $InstallParameters | Write-LSHotfixInstallStatus | Out-Null
    }

    # Sleep so installation status can be seen for a moment.
    Start-Sleep -s 2

    # Stop the clock.
    $EndTime = Get-Date

    # Calculate the duration.
    $Duration = NEW-TIMESPAN -Start $StartTime -End $EndTime 

    # Add installation results into $InstallParameters.
    $InstallParameters += @{"Hotfix Results" = 
        @{
            "success" = ($InstallParameters["HotfixInstallationErrors"].Count -eq 0);
            "duration" = $Duration
        }
    }

    return $InstallParameters
}


<#
    "Main"
#>

Write-LSHeader -Message "Welcome to the LS Insight setup"
start-sleep -s 1

Write-LSHeader -Message "Welcome to the LS Insight setup (...the rest of it)"
start-sleep -s 1.5

# Test if prerequisites are met.
Test-LSPrerequisites

# Clear hash-table in-case confirmation persists between script runs.
$InstallParameters = @{}

# Gather install parameters. 
while ($InstallParameters["confirmed"] -ne $true) {
    $InstallParameters = @{}
    $InstallParameters = $InstallParameters | Get-LSBaseFolder | Get-LSProjectFolder | Get-LSHotfixFolder | Get-LSInstallParametersConfirmation
}

$InstallParameters = $InstallParameters | Invoke-LSInsightHotFixes | Write-LSFooter